# This file is part of Appartition, a lightweight and portable backup tool.
# Copyright (C) 2012-2013 Simon Grieger
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

###########################################################################
# Class: Appartition::TableConfig
#
# Objects of this class store configuration read from an fstab-style
# configuration file.
#
# File Format:
#
#   While the rough fstab file format is the same across all platforms, the
#   details vary from implementation to implementation, especially
#   regarding the handling of special characters. For the sake of clarity,
#   here are some details specific to this implementation:
#
#   - Fields are strictly separated by whitespace characters, which means
#     quoting will not be recognized. Any apostrophe or quotation mark will
#     be taken literally. Some implementations seem to support quoting,
#     though.
#   - In order to include spaces or tabs (or any other special character)
#     in a field, you will have to escape them using a backslash (\),
#     followed by the character's three-digit octal ASCII value (e.g., \040
#     for a space character). This seems to be most common.
#   - To get a literal \040, one will have to escape the backslash in turn
#     (\134040). Escaping the backslash with another one (\\040) will not
#     work as expected. No implementation could be found actually allowing
#     for this anyway.
#   - Comments start with either a hash mark (#) or an asterisk (*). The
#     latter seems to be used by AIX.
#   - Unlike other implementations, comments are allowed to start
#     everywhere, not just at the beginning of a line. While this might be
#     a desirable feature, its drawback is that any hash mark or asterisk
#     supposed to appear in a field has to be escaped using the method
#     mentioned above (i.e., write them as \043 or \052, respectively).
#
#   In practice, these issues are likely to play little role.
###########################################################################

package Appartition::TableConfig;

use 5.006;
use strict;
use warnings;

###########################################################################
# Group: Constructors
###########################################################################

# Constructor: new
#
# Creates an object of class Appartition::TableConfig from the given
# configuration file. The file's information is stored in the object and
# made available through its methods.
#
# Parameters:
#
#   file - Path to the configuration file.
#
# Returns:
#
#   An object of class Appartition::TableConfig.
sub new #(file)
  {
    my $class = shift;
    my ($file) = @_;

    my $self = {
                'file'   => $file,
                'conf'   => [],
                'cursor' => 0,
               };

    bless($self, $class);

    # Read the configuration file.
    $self->read_file();

    return $self;
  }

###########################################################################
# Group: Accessor and Mutator Methods
###########################################################################

# Method: file
#
# Gets the path to the configuration file.
#
# Returns:
#
#   The path to the configuration file.
sub file #()
  {
    my $self = shift;
    return $self->{'file'};
  }

# Method: rows
#
# Gets the number of rows (entries) in the configuration file.
#
# Returns:
#
#   The number of entries.
sub rows #()
  {
    my $self = shift;
    return scalar @{$self->{'conf'}};
  }

# Method: columns
#
# Gets the number of columns (i.e., the number of fields each entry has) in
# the configuration file.
#
# Returns:
#
#   The number of fields.
sub columns #()
  {
    my $self = shift;
    my $entry = $self->{'conf'}[0];

    return defined $entry ? scalar @{$entry} : 0;
  }

###########################################################################
# Group: Methods for Reading the Configuration File
###########################################################################

# Method: get_entry
#
# Gets an entry from the configuration file. On each call to this function,
# an internal cursor will be incremented, so that a subsequent call will
# read the next entry, until there are no more entries left in the
# file. Use <rewind()> to reset the cursor and start over from the
# beginning.
#
# Returns:
#
#   A list of the current entry's fields, or the empty list if the cursor
#   is at EOF.
sub get_entry #()
  {
    my $self = shift;

    return () unless (defined $self->{'conf'}[$self->{'cursor'}]);
    return (@{$self->{'conf'}[$self->{'cursor'}++]});
  }

# Method: rewind
#
# Resets the internal cursor so that a subsequent call to <get_entry()>
# will start over at the beginning of the file.
#
# Returns:
#
#   True.
sub rewind #()
  {
    my $self = shift;

    $self->{'cursor'} = 0;
    return 1;
  }

###########################################################################
# Group: Internal Methods
#
# The methods in this group are not meant to be called directly. They are
# used internally by this class.
###########################################################################

# Method: read_file
#
# Reads the configuration file and stores its information in the object.
#
# Returns:
#
#   True.
sub read_file #()
  {
    my $self = shift;
    my $number = 1;

    # Be done if the file has already been read.
    return 1 if ($self->rows() > 0);

    open(my $config, '<', $self->file())
      or die 'Unable to open "' . $self->file() . '": ' . $! . "\n";

    while (my $line = <$config>) {
      $self->parse_line($line, $number);
      $number++;
    }

    close($config);

    return 1;
  }

# Method: parse_line
#
# Parses the given line (read from a configuration file) and stores its
# information in the object.
#
# Parameters:
#
#   line   - Line from a configuration file.
#   number - Line number. Used in error messages.
#
# Returns:
#
#   True.
sub parse_line #(line, number)
  {
    my $self = shift;
    my ($line, $number) = @_;

    # Strip comments and leading/trailing whitespace.
    $line =~ s'[#*].*$''o;
    $line =~ s'^\s+''o;
    $line =~ s'\s+$''o;

    # Skip comments and empty lines.
    return 1 if ($line eq '');

    # Split entry into fields.
    my @fields = split('\s+', $line);

    # Check number of fields.
    if ($self->rows() > 0 and @fields != $self->columns()) {
      die 'Invalid number of fields in line ' . $number . ' of file "' . $self->file() . '": ' .
          'Expected ' . $self->columns() . ', but encountered ' . @fields . "\n";
    }

    # Replace escaped special characters.
    foreach my $field (@fields) {
      $field =~ s/\\([0-7]{3})/chr(oct($1))/eg;
    }

    # Store the entry.
    push(@{$self->{'conf'}}, [@fields]);

    return 1;
  }

1;
